Bei Interrupts und Polling handelt es sich um zwei verschiedene Möglichkeiten, die unterschiedlichsten Ereignisse (Timer, externe Ereignisse) im Microcontroller zu
registrieren und zu verarbeiten. Es folgt eine kurze Beschreibung der beiden Methoden, Interrupts werden jedoch genauer beschrieben.
Hier schnell der Unterschied zwischen Interrupt und Polling in einer Grafik:
Polling überprüft direkt in der Hauptschleife des Programms, ob ein bestimmtes Ereignis eingetreten ist. Also: an einer Stelle im Programm ist eine Abfrage implementiert,
welche überprüft, ob das Ereignis aufgetreten ist. Sollte dies der Fall sein,
so wird dafür vorgesehener Code ausgeführt, der normalerweise mit einem rjmp
übersprungen worden werde.
Dadurch, dass die Abfrage beim Polling direkt in der Hauptschleife des Programms implementiert ist, wird dementsprechend auch nur einmal pro Schleifendurchlauf überprüft,
ob das Ereignis eingetreten ist, was zu einer verzögerten Reaktion führt.
Durch die Überprüfung, ob das Ereignis eingetreten ist, wird außerdem CPU-Zeit verwendet, was zu weiteren Verzögerungen für das restliche Programm führt.
Interrupts sind Programmstränge die parallel zum normalen Programm laufen und damit unabhängig vom Status der Hauptschleife sind. Sobald ein Ereignis auftritt,
wird sofort der Interrupt ausgelöst und behandelt, der normale Programmablauf wird hierfür an egal welcher Stelle unterbrochen.
Funktionsweise von Interrupts:
Jeder Art von Interrupt ist eine Adresse im Programmspeicher zugeordnet. Wird ein Interrupt ausgelöst, springt der Programmzeiger auf diese dem Interrupt zugewiesenen Adresse.
Diese Adresse befindet sich in der sogenannten Interruptvektortabelle, welche sich fast ganz am Anfang des Programmspeichers befindet.
Jeder Eintrag in der Interruptvektortabelle ist nur eine einzige Adresse lang. Darum muss in dieser ein rjmp
Befehl stehen, der dann zur eigentlichen auszuführenden
Stelle im Programmspeicher springt.
Im der obigen Grafik ist eine solche Vektortabelle zu sehen. Die importierte Konstante INT_VECTORS_SIZE
enthält hierbei die Adresse hinter der Vektortabelle.
Da nicht immer alle Interrupts benötigt werden, müssen nur die Interrupts angelegt werden, die später auch verwendet werden sollen.
Der im Beispiel (Grafik oben) dargestellte Interrupt INT0
(externer Interrupt 0) wird wie folgt erstellt:
Mit .include "tn2313def.inc"
wird eine Microcontroller-spezifische Datei geladen, welche Konstanten enthält, die im darauf folgenden Code verwendet werden können
(z.B. INT_VECTORS_SIZE
, INT0addr
oder ISC01
). Der nächste Befehl .org INT0addr
ist dafür zuständig, dass der Assembler als nächstes in die Adresse INT0addr
schreibt.
Hierbei ist es wichtig zu verstehen, dass es sich wirklich um eine Assembler-Direktive handelt, diese Zeile also am Ende nicht im Programmspeicher vorhanden sein wird.
Da vor dem dritten Befehl kein Punkt steht, dieser also keine Speicher-Direktive ist, wird die Assembler-Anweisung
tatsächlich in Maschinensprache umgewandelt und in den Programmspeicher geschrieben. Bei der Adresse INT0addr
steht nun also rjmp isrINT0
, nur eben in Maschinencode übersetzt.
Falls jetzt das Interrupt INT0
ausgelöst wird, springt der Programmzeiger direkt zur Adresse vom Interrupt 0: INT0addr
. Der Befehl, der in diesem Feld steht,
wird dementsprechend anschließend ausgeführt. Falls dort jedoch, sei es aus Unachtsamkeit des Programmierers, kein Befehl stehen sollte, geht der Programmzeiger
einfach Zeile für Zeile nach unten und führt die Befehle aus, auf die er stößt. Im Normalfall steht dort jedoch eine rjmp
-Anweisung (hier: rjmp isrINT0
),
mit der dann in die eigentliche Interruptbehandlung, zur sogenannten ISR, (Interrupt Service Routine) gesprungen wird.
ISR
Eine Interrupt service routine beginnt eigendlich immer mit einem Marker, zu dem gesprungen wird, und endet immer mit dem Befehl reti
,
der wieder zur ursprünglichen Adresse im Programmspeicher springt, die vor der Interruptbehandlung zuletzt ausgeführt wurde.
Zwischen diesen beiden Befehlen befinden sich normale Befehle.
Durch reti
wird der Interrupt also beendet. Steht am Ende der ISR kein reti
, springt der Programmzeiger nicht zurück in den Programmcode und führt so lange die Befehle aus,
auf die er stößt, bis er am Ende des Programmspeichers angelangt ist oder auf ein anderes reti
, womöglich von einer anderen ISR, stößt.
Vor Ausführung der eigentlichen Befehle in der ISR sollten wichtige Inhalte aus Registern (unter anderem das Statusregister SREG
) auf dem Stack (Stapelspeicher) gespeichert werden,
sodass diese Registerinhalte durch die Ausführung der ISR überschrieben und danach wiederhergestellt werden können. Auf diese Weise gehen keine Daten des Hauptprogramms verloren
und die ISR kann die Register so verwenden, wie sie es will. So werden auch zufällig erscheinende Probleme vermieden. Der Stack beginnt am Ende des SRAMs und wächst
dann Richtung Anfang, man setzt daher den Stackpointer zu Beginn auf die letzte Speicherzelle: es wird also zunächst die Adresse des Ende des RAMs gesucht.
Da die bei uns verwendeten Microcontroller 16 Bit-Adressen verwenden, setzt sich die Adresse aus einem oberen und einem unteren Bit zusammen, die je gesichert werden müssen.
Das wird wie folgt getan:
Somit befindet sich nun die Adresse in SPL
und SPH
. Für gewöhnlich findet das holen dieser Adressen ein mal am Anfang des Codes statt (und nicht in der Hauptschleife).
Das eigentliche Sichern der Register auf dem Stack erfolgt dann in den jeweiligen ISRs:
Was passiert hier?
Der Befehl push r16
schiebt zuerst den inhalt von r16
auf den Stack. Anschließend wird mit in r16, SREG
das Statusregister in r16
geladen.
Dieses wird dann wieder mit push r16
auf den Stack geschoben.
Es folgen die eigentlichen Befehle der Interruptbehandlung. Um die gesicherten Daten der Register wieder aus dem Stack in die eigentlichen Variablen zu verschieben
muss man zuerst das Register r16
mit dem SREG
vom Stapel holen, da dieses ganz oben liegt (mit dem Befehl pop r16
). Dann speichert man den Wert wieder in SREG
mit in r16, SREG
. Anschließend holt man noch das eigentliche Register r16
aus dem Stack mit pop r16
.
Hier ist der Ablauf eines solchen Interrupts (hier für das eben implementierte INT0
) einmal zu sehen:
Nun haben wir für unseren Fall die ISR fertig implementiert.
Allgemein sind folgende Schritte notwendig, um ein Interrupt zu initialisieren:
Es gibt verschiedene Arten von Interrupts, die alle bei unterschiedlichen Dingen auslösen. Die für uns relevanten sind
Externe Interrupts und Timer-Interrupts.